package cool.webstudy.admin.filter;

import cn.hutool.core.map.MapUtil;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import cool.webstudy.admin.model.dto.user.UserDetailInfoDTO;
import cool.webstudy.admin.service.ApiLoggerService;
import cool.webstudy.admin.utils.RequestUtil;
import cool.webstudy.admin.utils.UserDetailUtil;
import cool.webstudy.common.constant.CommonConstant;
import cool.webstudy.common.constant.enums.ApiActionMethodEnum;
import cool.webstudy.common.constant.enums.OperationalStatusEnum;
import cool.webstudy.common.model.dto.logger.api.CreateApiLogDBDTO;
import cool.webstudy.common.model.dto.logger.api.CreateApiLogESDTO;
import cool.webstudy.common.model.po.logger.api.ApiLoggerPO;
import cool.webstudy.common.utils.UUIDUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.CollectionUtils;
import org.springframework.util.PathMatcher;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import org.springframework.web.util.WebUtils;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.*;

/**
 * @author 莫振双
 * @date 2024/7/25 23:20
 * @description: 接口请求日志记录过滤器
 */
@Component
@WebFilter(filterName = "HttpServletRequestFilter", urlPatterns = "/")
@Order(0)
public class ApiLoggerFilter extends OncePerRequestFilter {
    @Autowired
    private ApiLoggerService apiLoggerService;
    
    private static final Logger logger = LoggerFactory.getLogger(ApiLoggerFilter.class);
    @Value("#{'${security.url_white_list}'.split(',')}")
    private String[] excludeUris;
    @Value("${server.servlet.context-path:/api}")
    private String contextPath;
    private static final PathMatcher URI_PATH_MATCHER = new AntPathMatcher();
    private static final List<String> DEFAULT_DOWNLOAD_CONTENT_TYPE = new ArrayList<String>(Arrays.asList(
            "application/vnd.ms-excel",//.xls
            "application/msexcel",//.xls
            "application/cvs",//.cvs
            MediaType.APPLICATION_OCTET_STREAM_VALUE,//.*（ 二进制流，不知道下载文件类型）
            "application/x-xls",//.xls
            "application/msword",//.doc
            MediaType.TEXT_PLAIN_VALUE,//.txt
            "application/x-gzip"//.gz
    ));

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        final boolean isFirstRequest = !isAsyncDispatch(request);

        final boolean shouldWrapRequest = isFirstRequest;
        final HttpServletRequest requestCopy = shouldWrapRequest ? new ContentCachingRequestWrapper(request) : request;

        final HttpServletResponse responseCopy = new ContentCachingResponseWrapper(response);

        final long startTime = System.currentTimeMillis();
        Throwable t = null;
        try {
            filterChain.doFilter(requestCopy, responseCopy);
        } catch (Exception e) {
            t = e;
            throw e;
        } finally {
            // 如果不是被排除的uri，则记录日志
            if (!matchExclude(request.getRequestURI())) {
                doSaveLog(requestCopy, responseCopy, System.currentTimeMillis() - startTime, t);
            }else{
                //如果是排除的uri，但是返回的异常信息不为空，则移除异常信息
                removeResponseField(responseCopy, CommonConstant.HTTP_RESPONSE_EXCEPTION_KEY);
                copyResponse(responseCopy);
            }

        }
    }

    /**
     * @param request
     * @param response
     * @param useTime
     * @param t
     * @description: 保存日志方法
     */
    private void doSaveLog(final HttpServletRequest request,
                           final HttpServletResponse response,
                           final long useTime,
                           final Throwable t) throws IOException {
        if (isAsyncStarted(request)) {
            copyResponse(response);
            return;
        }
        try {
            ApiLoggerPO apiLoggerPO = new ApiLoggerPO();
            apiLoggerPO.setUnCode(UUIDUtil.getUUID());
            apiLoggerPO.setTraceId(UUIDUtil.getUUID());
            apiLoggerPO.setOrderId(0);
            apiLoggerPO.setCreateTime(new Date());
            final String requestMethod = request.getMethod();
            apiLoggerPO.setApiActionMethod(ApiActionMethodEnum.valueOf(requestMethod));
            final String requestUri = request.getRequestURI();
            final String requestHeaders = getRequestHeaders(request);
            final String requestIp = RequestUtil.getRequestIp(request);
            final String requestParams = getRequestParams(request);
            final String requestString = isUpload(request) ? StringUtils.EMPTY : getRequestString(request);
            final int responseStatus = response.getStatus();
            //将异常信息从响应体中移出至exceptionNode
            JsonNode exceptionNode = removeResponseField(response, CommonConstant.HTTP_RESPONSE_EXCEPTION_KEY);
            apiLoggerPO.setCostTime(useTime);
            apiLoggerPO.setSourceIp(requestIp);
            apiLoggerPO.setApiPath(requestUri);
            apiLoggerPO.setRequestStatus(responseStatus);
            if (StringUtils.isNotBlank(requestParams)) {
                apiLoggerPO.setRequestBody(requestParams);
            }
            if (StringUtils.isNotBlank(requestString)) {
                apiLoggerPO.setRequestBody(requestString);
            }
            final String responseString = isDownload(response) ? StringUtils.EMPTY : getResponseString(response);
            apiLoggerPO.setResponseBody(responseString);
            OperationalStatusEnum operationalStatusEnum = OperationalStatusEnum.SUCCESS;
            if (Objects.nonNull(exceptionNode) && Boolean.FALSE.equals(exceptionNode.toString().equals("null"))) {
                apiLoggerPO.setException(exceptionNode.toString());
                operationalStatusEnum = OperationalStatusEnum.FAILURE;
            }
            apiLoggerPO.setOperationalStatus(operationalStatusEnum);

            UserDetailInfoDTO userDetailInfoDTO;
            try {
                userDetailInfoDTO =  UserDetailUtil.getUserDetailInfo();
                apiLoggerPO.setOperator(userDetailInfoDTO.getAccount());
            }catch (Exception e) {
                apiLoggerPO.setOperator(CommonConstant.ANONYMOUS_USER_ACCOUNT);
            }
            //构建创建日志到数据库的DTO
            CreateApiLogDBDTO apiLogDBDTO = new CreateApiLogDBDTO();
            apiLogDBDTO.setTraceId(apiLoggerPO.getTraceId());
            apiLogDBDTO.setOperator(apiLoggerPO.getOperator());
            apiLogDBDTO.setOrderId(apiLoggerPO.getOrderId());
            apiLogDBDTO.setSourceIp(apiLoggerPO.getSourceIp());
            apiLogDBDTO.setRequestStatus(apiLoggerPO.getRequestStatus());
            apiLogDBDTO.setCostTime(apiLoggerPO.getCostTime());
            apiLogDBDTO.setApiActionMethod(apiLoggerPO.getApiActionMethod());
            apiLogDBDTO.setApiPath(apiLoggerPO.getApiPath());
            apiLogDBDTO.setCreateTime(apiLoggerPO.getCreateTime());
            apiLogDBDTO.setOperationalStatus(apiLoggerPO.getOperationalStatus());
            //apiLoggerService.sendApiLogToDBMQ(apiLogDBDTO);
            //构建创建日志到ES的DTO
            CreateApiLogESDTO apiLogESDTO = new CreateApiLogESDTO();
            apiLogESDTO.setTraceId(apiLoggerPO.getTraceId());
            apiLogESDTO.setRequestBody(apiLoggerPO.getRequestBody());
            apiLogESDTO.setResponseBody(apiLoggerPO.getResponseBody());
            apiLogESDTO.setException(apiLoggerPO.getException());
            apiLogESDTO.setCreateTime(apiLoggerPO.getCreateTime());
            //apiLoggerService.sendApiLogToESMQ(apiLogESDTO);
        } catch (Throwable e) {
            logger.error("got an exception when saving access log", e);
        } finally {
            copyResponse(response);
        }
    }



    private String getRequestHeaders(final HttpServletRequest request) {
        final Enumeration<String> headerNames = request.getHeaderNames();
        final List<String> headers = new ArrayList<>();
        while (headerNames.hasMoreElements()) {
            final String key = headerNames.nextElement();
            headers.add(key + ':' + request.getHeader(key));
        }
        return '[' + String.join(",", headers) + ']';
    }

    private String getRequestParams(final HttpServletRequest request) {
        final Map<String, String[]> requestParams = new HashMap<>(request.getParameterMap());
        final List<String> pairs = new ArrayList<>();
        if (MapUtil.isNotEmpty(requestParams)) {
            for (final Map.Entry<String, String[]> entry : requestParams.entrySet()) {
                final String name = entry.getKey();
                final String[] value = entry.getValue();
                if (value == null) {
                    pairs.add(name + "=");
                } else {
                    for (final String v : value) {
                        pairs.add(name + "=" + StringUtils.trimToEmpty(v));
                    }
                }
            }
        }
        String requestParamsStr = CollectionUtils.isEmpty(pairs) ? StringUtils.EMPTY : String.join("&", pairs);
        if (StringUtils.equalsIgnoreCase(request.getContentType(), MediaType.APPLICATION_FORM_URLENCODED_VALUE)) {
            try {
                requestParamsStr = URLDecoder.decode(requestParamsStr, StandardCharsets.UTF_8.name());
            } catch (UnsupportedEncodingException ignored) {
            }
        }
        return requestParamsStr;
    }

    private boolean isUpload(final HttpServletRequest request) {
        final String contentType = request.getHeader(HttpHeaders.CONTENT_TYPE);
        if (StringUtils.isBlank(contentType)) {
            return false;
        }
        return StringUtils.containsIgnoreCase(contentType, MediaType.MULTIPART_FORM_DATA_VALUE);
    }

    private boolean isDownload(final HttpServletResponse response) {
        final String contentType = response.getContentType();
        if (StringUtils.isBlank(contentType)) {
            return false;
        }
        return DEFAULT_DOWNLOAD_CONTENT_TYPE.stream().anyMatch(it -> StringUtils.equalsIgnoreCase(it, contentType));
    }

    private String getRequestString(final HttpServletRequest request) {
        final ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
        if (wrapper != null) {
            try {
                final byte[] buf = wrapper.getContentAsByteArray();
                return new String(buf, wrapper.getCharacterEncoding()).replaceAll("\n|\r", "");
            } catch (UnsupportedEncodingException e) {
                return StringUtils.EMPTY;
            }
        }
        return StringUtils.EMPTY;
    }

    private String getResponseString(final HttpServletResponse response) {
        final ContentCachingResponseWrapper wrapper = WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
        if (wrapper != null) {
            try {
                final byte[] buf = wrapper.getContentAsByteArray();
                return new String(buf, wrapper.getCharacterEncoding()).replaceAll("\n|\r", "");
            } catch (UnsupportedEncodingException e) {
                return StringUtils.EMPTY;
            }
        }
        return StringUtils.EMPTY;
    }

    private void copyResponse(final HttpServletResponse response) {
        final ContentCachingResponseWrapper wrapper = WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
        if (wrapper != null) {
            try {
                wrapper.copyBodyToResponse();
            } catch (IOException ignored) {
            }
        }
    }

    private boolean matchExclude(final String uri) {
        //移除掉uri前缀
        String uriWithoutPrefix = uri.replace(contextPath, "");
        if (ArrayUtils.isEmpty(excludeUris)){
            return false;
        }
        for (final String excludeUri : excludeUris) {
            if (URI_PATH_MATCHER.match(excludeUri, uriWithoutPrefix)) {
                return true;
            }
        }
        return false;
    }


    /**
     * @param response
     * @param fieldName
     * @description: 移除响应字段方法
     */
    private JsonNode removeResponseField(HttpServletResponse response, String fieldName) throws IOException {
        if (response.isCommitted()) {
            return null;
        }
        JsonNode exceptionNode = null;
        ObjectMapper objectMapper = new ObjectMapper();
        JsonNode jsonNode = null;
        try {
            String responseStr = getResponseString(response);
            jsonNode = objectMapper.readTree(responseStr);
        }catch (Exception ignored){
            //忽略异常
            return null;
        }
        exceptionNode = ((ObjectNode) jsonNode).remove(fieldName);
        //先清空原有的响应信息
        response.resetBuffer();
        //写入新的响应信息
        objectMapper.writeValue(response.getOutputStream(), jsonNode);
        return exceptionNode;
    }
}
